use crate::model::user::{
    generate_hmac, LoginSecret, PasswordCredential, PasswordSecret, Salt, UserCredentials,
    UserData, UserId, UserIdNum, UserIdentifier, UserInfo, UserLoginCredential, UserLoginSecret,
    Username, SALT_LENGTH,
};
use rand::{rngs::ThreadRng, RngCore};
use rocket_db_pools::sqlx::{self, sqlite::SqliteRow, Row, Sqlite, Transaction};
use std::cmp::Ordering;

pub enum DeleteStatus {
    Deleted,
    NotFound,
}

pub enum UpdateUsernameError {
    ExistingUsername,
    UserNotFound,
    DatabaseError(sqlx::Error),
    InvalidUsername,
}

impl From<sqlx::Error> for UpdateUsernameError {
    fn from(err: sqlx::Error) -> Self {
        UpdateUsernameError::DatabaseError(err)
    }
}

#[derive(Debug)]
pub enum UpdatePasswordError {
    EmptyPassword,
    DatabaseError(sqlx::Error),
    SaltRngFailed(rand::Error),
}

impl From<sqlx::Error> for UpdatePasswordError {
    fn from(err: sqlx::Error) -> Self {
        UpdatePasswordError::DatabaseError(err)
    }
}

impl From<rand::Error> for UpdatePasswordError {
    fn from(err: rand::Error) -> Self {
        UpdatePasswordError::SaltRngFailed(err)
    }
}

pub enum UpdateUserInfoError {
    DatabaseError(sqlx::Error),
}

impl From<sqlx::Error> for UpdateUserInfoError {
    fn from(err: sqlx::Error) -> Self {
        UpdateUserInfoError::DatabaseError(err)
    }
}

pub trait UserIdExt {
    async fn get_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<UserData>, sqlx::Error>;

    async fn get_credentials<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<UserCredentials, sqlx::Error>;

    async fn update_username<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        new_username: Username,
    ) -> Result<(), UpdateUsernameError>;

    async fn update_user_password<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        new_password: PasswordSecret,
    ) -> Result<(), UpdatePasswordError>;

    async fn update_user_info<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        info: UserInfo,
    ) -> Result<(), UpdateUserInfoError>;

    async fn delete_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<DeleteStatus, sqlx::Error>;
}

impl UserIdExt for UserId {
    async fn get_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<UserData>, sqlx::Error> {
        sqlx::query("SELECT id, username, is_admin FROM users WHERE id = ?")
            .bind(self.0)
            .try_map(|row: SqliteRow| {
                Ok(UserData {
                    identifier: UserIdentifier {
                        id: UserId(row.try_get("id")?),
                        username: Username(row.try_get("username")?),
                    },
                    info: UserInfo {
                        is_admin: row.try_get("is_admin")?,
                    },
                })
            })
            .fetch_optional(&mut **db)
            .await
    }

    async fn get_credentials<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<UserCredentials, sqlx::Error> {
        sqlx::query("SELECT id, password_hash, password_salt FROM users WHERE id = ?")
            .bind(self.0)
            .try_map(|row: SqliteRow| {
                let password_credential = {
                    let password_hash: Option<Vec<u8>> = row.try_get("password_hash")?;
                    let password_salt: Option<Vec<u8>> = row.try_get("password_salt")?;
                    convert_password_credential(password_hash, password_salt)
                };
                Ok(UserCredentials {
                    password: password_credential,
                })
            })
            .fetch_one(&mut **db)
            .await
    }

    async fn update_username<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        new_username: Username,
    ) -> Result<(), UpdateUsernameError> {
        if !new_username.is_valid() {
            return Err(UpdateUsernameError::InvalidUsername);
        }

        let user = self.get_user(db).await?;
        if user.is_none() {
            return Err(UpdateUsernameError::UserNotFound);
        }

        let user = sqlx::query("SELECT username FROM users WHERE username = ? AND id <> ?")
            .bind(&new_username.0)
            .bind(self.0)
            .fetch_optional(&mut **db)
            .await?;
        if user.is_some() {
            return Err(UpdateUsernameError::ExistingUsername);
        }

        sqlx::query(
            r#"
            UPDATE users
            SET username = ?
            WHERE id = ?
        "#,
        )
        .bind(new_username.0)
        .bind(self.0)
        .execute(&mut **db)
        .await?;

        Ok(())
    }

    async fn update_user_password<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        new_password: PasswordSecret,
    ) -> Result<(), UpdatePasswordError> {
        if !new_password.is_valid() {
            return Err(UpdatePasswordError::EmptyPassword);
        }

        let password_salt = generate_hash()?;
        let password_hmac = generate_hmac(&new_password.0, &password_salt).await;

        sqlx::query(
            r#"
            UPDATE users
            SET
                password_hash = ?,
                password_salt = ?
            WHERE id = ?
        "#,
        )
        .bind(password_hmac)
        .bind(&password_salt[..])
        .bind(self.0)
        .execute(&mut **db)
        .await?;
        Ok(())
    }

    async fn update_user_info<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
        info: UserInfo,
    ) -> Result<(), UpdateUserInfoError> {
        sqlx::query(
            r#"
            UPDATE users
            SET
                is_admin = ?
            WHERE id = ?
        "#,
        )
        .bind(info.is_admin)
        .bind(self.0)
        .execute(&mut **db)
        .await?;
        Ok(())
    }

    async fn delete_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<DeleteStatus, sqlx::Error> {
        let deleted_rows = sqlx::query("DELETE FROM users WHERE id = ?")
            .bind(self.0)
            .execute(&mut **db)
            .await?;

        match deleted_rows.rows_affected().cmp(&1) {
            Ordering::Less => Ok(DeleteStatus::NotFound),
            Ordering::Equal => Ok(DeleteStatus::Deleted),
            Ordering::Greater => panic!("Deleted more than one row"),
        }
    }
}

fn map_user_row(row: SqliteRow) -> Result<UserData, sqlx::Error> {
    Ok(UserData {
        identifier: UserIdentifier {
            id: UserId(row.try_get("id")?),
            username: Username(row.try_get("username")?),
        },
        info: UserInfo {
            is_admin: row.try_get("is_admin")?,
        },
    })
}

pub trait UsernameExt {
    fn is_valid(&self) -> bool;

    async fn get_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<UserData>, sqlx::Error>;
}

impl UsernameExt for Username {
    fn is_valid(&self) -> bool {
        !self.0.is_empty()
    }

    async fn get_user<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<UserData>, sqlx::Error> {
        sqlx::query("SELECT id, username, is_admin FROM users WHERE username = ?")
            .bind(self.0)
            .try_map(map_user_row)
            .fetch_optional(&mut **db)
            .await
    }
}

fn convert_password_credential(
    hash: Option<Vec<u8>>,
    salt: Option<Vec<u8>>,
) -> Option<PasswordCredential> {
    let hash = hash?;
    let salt = salt?;
    if let Ok(salt) = salt.try_into() {
        Some(PasswordCredential { hash, salt })
    } else {
        None
    }
}

impl UserLoginCredential {
    pub async fn login<'a>(
        self,
        db: &mut Transaction<'a, Sqlite>,
    ) -> Result<Option<UserData>, sqlx::Error> {
        let userdata = self.username.get_user(db).await?;
        let userdata = match userdata {
            Some(userdata) => userdata,
            None => return Ok(None),
        };
        let credentials = userdata.identifier.id.get_credentials(db).await?;
        match self.secret {
            UserLoginSecret::Password(secret) => {
                if let Some(ref credential) = credentials.password {
                    if secret.check(credential).await {
                        return Ok(Some(userdata));
                    }
                }
            }
        }
        Ok(None)
    }
}

pub async fn get_users<'a>(db: &mut Transaction<'a, Sqlite>) -> Result<Vec<UserData>, sqlx::Error> {
    sqlx::query("SELECT id, username, is_admin FROM users ORDER BY id")
        .try_map(map_user_row)
        .fetch_all(&mut **db)
        .await
}

fn generate_hash() -> Result<Salt, rand::Error> {
    let mut rand = ThreadRng::default();
    let mut salt: Salt = [0; SALT_LENGTH];
    rand.try_fill_bytes(&mut salt)?;
    Ok(salt)
}

#[derive(Debug)]
pub enum CreateError {
    ExistingUsername,
    InvalidUsername,
    DatabaseError(sqlx::Error),
}

impl From<sqlx::Error> for CreateError {
    fn from(err: sqlx::Error) -> Self {
        CreateError::DatabaseError(err)
    }
}

pub async fn create_user<'a>(
    db: &mut Transaction<'a, Sqlite>,
    username: Username,
    info: UserInfo,
) -> Result<UserData, CreateError> {
    if !username.is_valid() {
        return Err(CreateError::InvalidUsername);
    }
    let username_exists = sqlx::query("SELECT EXISTS (SELECT 1 FROM users WHERE username = ?)")
        .bind(&username.0)
        .map(|row: SqliteRow| row.get(0))
        .fetch_one(&mut **db)
        .await?;
    if username_exists {
        return Err(CreateError::ExistingUsername);
    }

    sqlx::query(
        r#"
            INSERT INTO users
            (username, is_admin) VALUES (?, ?)
        "#,
    )
    .bind(&username.0)
    .bind(info.is_admin)
    .execute(&mut **db)
    .await?;

    let id: UserIdNum = sqlx::query("SELECT LAST_INSERT_ROWID()")
        .map(|row: SqliteRow| row.get(0))
        .fetch_one(&mut **db)
        .await?;

    Ok(UserData {
        identifier: UserIdentifier {
            id: UserId(id),
            username,
        },
        info,
    })
}
